默认布局:头像菜单组件事件定义与传递
事件传递是组件化开发中最容易出错的环节。在 Header -> AvatarMenu -> DefaultLayout 的三层结构中,每一层都需要正确地转发事件和属性。本节聚焦于 AvatarMenu 的 divider 分割线实现、Header 的属性透传机制,以及多层组件间事件的规范化传递方式。
divider 分割线的实现
AvatarMenu 的下拉菜单中,菜单项之间可能需要分割线。通过数据驱动的方式控制分割线的显示位置:
const menuData = [
{ key: 'profile', value: '个人中心' },
{ key: 'divider', value: '' }, // 分割线
{ key: 'logout', value: '退出登录' },
]
typescript
模板中遍历时根据 key 值判断:
<template v-for="menu in data" :key="menu.key ?? menu">
<!-- 分割线 -->
<el-divider
v-if="(menu as any).key === 'divider'"
class="my-0!"
/>
<!-- 菜单项 -->
<el-dropdown-item v-else :command="getCommand(menu)">
{{ getLabel(menu) }}
</el-dropdown-item>
</template>
vue
注意 v-for 放在外层的 template 上,而不是分别放在 el-divider 和 el-dropdown-item 上,这样可以用 v-if/v-else 控制二选一渲染。
Header 的属性透传
Header 组件需要接收 AvatarMenu 的所有配置属性(data、username、src 等),同时自身还有 collapse 等属性。使用 computed 分离出 AvatarMenu 专用 props:
// header.vue
interface HeaderProps {
collapse?: boolean
username?: string
src?: string
data?: DropDownMenuItem[]
// ...其他 AvatarMenu 属性
}
const avatarProps = computed(() => {
const { collapse, locales, ...rest } = props
return rest
})
typescript
模板中使用 v-bind 一次性绑定:
<AvatarMenu v-bind="avatarProps" @command="handleCommand" />
vue
Partial 的使用
Header 的属性大多为可选,使用 Partial 包装:
interface HeaderProps {
collapse?: boolean
username?: string
src?: string
data?: DropDownMenuItem[]
}
defineProps<Partial<HeaderProps>>()
typescript
当 username 和 src 都未设置时,整个 AvatarMenu 区域不渲染:
<AvatarMenu
v-if="username || src"
v-bind="avatarProps"
/>
vue
多层事件传递的问题
在三层组件结构中,事件的传递路径是:
DefaultLayout
└── Header (@command="handleCommand")
└── AvatarMenu (@command="handleCommand")
text
每一层都需要 defineEmits + emit 转发。这种层层转发的方式在组件层级较深时确实繁琐。
为什么不使用 Provide/Inject 传递事件?
理论上可以用 provide 在根组件注入一个事件处理函数,子组件通过 inject 调用。但这样做有以下问题:
- 组件独立性丧失:AvatarMenu 从"自包含的独立组件"变成了"必须依赖上级 provide"的组件。
- 复用困难:如果用户基于 AvatarMenu 创建新组件,新组件无法获得 provide 的事件。
- 类型安全降低:
inject的返回值类型不如defineEmits精确。
因此,在 Header 和 AvatarMenu 这种层级较少的场景中,保留逐层转发事件是更好的选择。只有当组件层级非常深(5 层以上)时,才考虑使用 Provide/Inject。
defineModel 的使用场景
collapse 属性既需要从 Header 传入(控制折叠状态),又需要从 Header 传出(用户点击折叠按钮)。这是典型的双向绑定场景,使用 Vue 3.4+ 的 defineModel:
// header.vue
const collapseModel = defineModel<boolean>('collapse', {
default: false,
})
typescript
模板中直接绑定:
<Iconify
:icon="collapseModel ? 'ep:expand' : 'ep:fold'"
@click="collapseModel = !collapseModel"
/>
vue
父组件中使用 v-model 绑定:
<Header v-model:collapse="isCollapsed" />
vue
默认值设置的注意事项
withDefaults 对 defineModel 的默认值设置方式不同。defineModel 的默认值直接在第二个参数中设置:
// 正确写法
const collapseModel = defineModel<boolean>('collapse', {
default: false,
})
// 错误写法(withDefaults 不适用于 defineModel)
withDefaults(defineModel('collapse'), { default: false })
typescript
本节小结
- divider 分割线:数据驱动,通过
key === 'divider'判断渲染位置。 - 属性透传:使用
computed分离出子组件需要的 props,通过v-bind绑定。 - 事件转发:在层级较少时保留逐层
defineEmits转发,保证组件独立性。 - defineModel:用于双向绑定场景(如 collapse 折叠状态),简化父子组件的值传递。
- Partial:Header 的 props 大多为可选,使用
Partial放宽类型约束。
↑